package cn.mesmile.admin.modules.system.service.impl;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.mesmile.admin.common.utils.AdminRedisTemplate;
import cn.mesmile.admin.modules.system.service.ISignInService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;


/**
 * @author zb
 * @Description
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class SignInServiceImpl implements ISignInService {

    private final AdminRedisTemplate adminRedisTemplate;

    /**
   【利用 redis 位图，实现签到功能】
    # 用户2月17号签到
    SETBIT u:sign:1000:202202 16 1 # 偏移量是从0开始，所以要把17减1
    # 检查2月17号是否签到
    GETBIT u:sign:1000:202202 16 # 偏移量是从0开始，所以要把17减1
    # 统计2月份的签到次数
    BITCOUNT u:sign:1000:202202
    # 获取2月份前28天的签到数据
    BITFIELD u:sign:1000:202202 get u28 0
    # 获取2月份首次签到的日期
    BITPOS u:sign:1000:202202 1 # 返回的首次签到的偏移量，加上1即为当月的某一天
     */

    @Override
    public boolean signIn(Long userId, LocalDate localDate) {
        int dayOfMonth = localDate.getDayOfMonth();
        String key = getSignInKey(userId, localDate);
        // 操作成功默认返回 FALSE
        Boolean result = adminRedisTemplate.getValueOps().setBit(key, dayOfMonth - 1, true);
        return result != null;
    }

    private String getSignInKey(Long userId, LocalDate localDate) {
        // example  user:sign:123456:202210
        return StrUtil.format("user:sign:{}:{}", userId, LocalDateTimeUtil.format(localDate, DatePattern.SIMPLE_MONTH_FORMATTER));
    }

    @Override
    public boolean checkSignIn(Long userId, LocalDate localDate) {
        int dayOfMonth = localDate.getDayOfMonth();
        Boolean result = adminRedisTemplate.getValueOps().getBit(getSignInKey(userId, localDate), dayOfMonth - 1);
        return Optional.ofNullable(result).orElse(false);
    }

    @Override
    public int getSignInCount(Long userId, LocalDate localDate) {
        Long result = adminRedisTemplate.getRedisTemplate().execute(
                (RedisCallback<Long>) con -> con.bitCount(getSignInKey(userId, localDate).getBytes(StandardCharsets.UTF_8))
        );
        return Convert.toInt(result);
    }

    @Override
    public int getSignInContinuityCount(Long userId, LocalDate localDate) {
        int signCount = 0;
        int dayOfMonth = localDate.getDayOfMonth();
        BitFieldSubCommands.BitFieldGet bitFieldGet = BitFieldSubCommands.BitFieldGet.create(
                // 获取这个月的总天数，最多批量支持获取 64 位,最大支持无符号 u63 位
                BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth),
                // 从0开始
                BitFieldSubCommands.Offset.offset(0)
        );
        List<Long> longs = adminRedisTemplate.getValueOps().bitField(getSignInKey(userId, localDate), BitFieldSubCommands.create(bitFieldGet));
        if (longs != null && longs.size() > 0) {
            // 取低位连续不为0的个数即为连续签到次数，需考虑当天尚未签到的情况
            long v = longs.get(0) == null ? 0 : longs.get(0);
            for (int i = 0; i < dayOfMonth; i++) {
                // 先右移动一位再左移动一位，若不相等则是有 1 被移出
                if (v >> 1 << 1 == v) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                // 二进制向右移动一位，准备比较下一位
                v = v >> 1;
            }
        }
        int offset = -1;
        int count = 1;
        // 上个月
        LocalDateTime offsetLocalDate = LocalDateTimeUtil.offset(LocalDateTime.now(), offset, ChronoUnit.MONTHS);
        int daysOfMonth = offsetLocalDate.getDayOfMonth();
        int days = dayOfMonth + daysOfMonth;
        if (signCount == dayOfMonth) {
            // 当代码中使用递归时碰到了想中途退出递归,但是代码继续执行的情况,抛出异常上层捕获,避免跳出递归获取的值不正确问题
            try {
                getSignCount(userId, signCount, offset, count, days);
            } catch (Exception e) {
                String message = e.getMessage();
                if (NumberUtil.isNumber(message)){
                    signCount = Integer.parseInt(message);
                }
            }
        }
        return signCount;
    }

    private int getSignCount(Long userId, int signCount, int offset, int count, int days) throws Exception {
        // 上上个月
        LocalDateTime dateTime1 = LocalDateTimeUtil.offset(LocalDateTime.now(), offset * count, ChronoUnit.MONTHS);
        // 获取上上个月的天数
        LocalDate localDate = LocalDateTimeUtil.ofDate(dateTime1);
        int currentTotalDays = localDate.lengthOfMonth();
        BitFieldSubCommands.BitFieldGet bitFieldGet = BitFieldSubCommands.BitFieldGet.create(
                // 获取这个月的总天数，最多批量支持获取 64 位
                BitFieldSubCommands.BitFieldType.unsigned(currentTotalDays),
                // 从0开始
                BitFieldSubCommands.Offset.offset(0)
        );
        List<Long> lastList = adminRedisTemplate.getValueOps().bitField(getSignInKey(userId, localDate), BitFieldSubCommands.create(bitFieldGet));

        if (lastList != null && lastList.size() > 0) {
            // 取低位连续不为0的个数即为连续签到次数，需考虑当天尚未签到的情况
            long v = lastList.get(0) == null ? 0 : lastList.get(0);

            for (int i = 0; i < currentTotalDays; i++) {
                if (v >> 1 << 1 == v) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
            count += 1;
        }
        // 如果连续签到次数小于了当前月天数+多个整月天数，证明连续签到中断
        if (signCount < days) {
            throw new Exception(String.valueOf(signCount));
        }
        // 当前月总的天数+上个月的天数
        LocalDateTime localDateTime = LocalDateTimeUtil.offset(LocalDateTime.now(), offset * (count - 1), ChronoUnit.MONTHS);
        days = days + LocalDateTimeUtil.ofDate(localDateTime).lengthOfMonth();
        getSignCount(userId, signCount, offset, count, days);
        return signCount;
    }

    @Override
    public LocalDate getFirstSignInByDate(Long userId, LocalDate localDate) {
        Long resultCount = adminRedisTemplate.getRedisTemplate().execute((RedisCallback<Long>) con ->
                con.bitPos(getSignInKey(userId, localDate).getBytes(StandardCharsets.UTF_8), true));
        long result = Optional.ofNullable(resultCount).orElse(-1L);
        return result < 0 ? null : localDate.withDayOfMonth((int) (result + 1));
    }

    @Override
    public Map<LocalDate, Boolean> getSignInInfo(Long userId, LocalDate localDate) {
        int days = localDate.lengthOfMonth();
        Map<LocalDate, Boolean> resultMap = new LinkedHashMap<>(days);
        BitFieldSubCommands.BitFieldGet bitFieldGet = BitFieldSubCommands.BitFieldGet.create(
                // 获取这个月的总天数，最多批量支持获取 64 位, 无符号数最大只能取63位，也就是一次最多只能取63天的签到数据
                BitFieldSubCommands.BitFieldType.unsigned(days),
                // 从0开始
                BitFieldSubCommands.Offset.offset(0)
        );
        List<Long> longs = adminRedisTemplate.getValueOps().bitField(getSignInKey(userId, localDate), BitFieldSubCommands.create(bitFieldGet));
        if (longs != null && longs.size() > 0) {
            // 由低位到高位，为0表示未签，为1表示已签
            long v = longs.get(0) == null ? 0 : longs.get(0);
            for (int i = localDate.lengthOfMonth(); i > 0; i--) {
                LocalDate d = localDate.withDayOfMonth(i);
                // 234881024 转换为 二进制如下，获取无符号的 u31 位
                // 0001110  00000000   00000000   00000000
                // 10月1日  -------------————  10月31日（每个零代表一天）
                // 先右移动一位 然后再左移动一位，若不相等，则有 1 被移出（因为左移动会在右边自动补充零）
                resultMap.put(d, v >> 1 << 1 != v);
                // 向右移动一位，检查下一天
                v = v >> 1;
            }
        }
        return resultMap;
    }


}
